#!/usr/bin/env python

##  flamingo - 2D Game Engine
##  Copyright (C) 2009 Bradley Zeis
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##  Bradley Zeis
##  flamingoengine@gmail.com

"""
Terms:
    Action      - Something that happens generally, not something specific.
    Event       - A specific instance of an action that is placed in the EventRecord.
    Frame       - One cycle of even processing. Events are handled once per display frame.
    Reaction    - A callback called because of a specific event. You 'bind' reactions to EventListeners.
    Modifiers   - Events that 'modify' reactions. For example, pressing shift can cause a
                  different reaction for mouse clicks.
                  
All times are the time of the frame when the events are handled, not the time when the event occurred. Backends
do not need to worry about what time the even happened, they just need to preserve order. (The frontend gets
the time from core._Data.current_time)


Event backends must define:

    init()
    poll()
"""

import sys

from . import constants
from . import core

_BACKEND = None
_INITIALIZED = False
RECORD = None
POSTED = {}

def load(event_name):
    """Load an event backend.
    
    If the event backend has already been initialized, there will be no effect.
    
    Return: None
    """
    if _INITIALIZED:
        return
    
    try:
        global _BACKEND
        __import__(event_name)
        _BACKEND =  sys.modules[event_name]
        
    except ImportError:
        raise ImportError, "could not import backend '{0}'".format(event_name)

def init():
    """Initialize the event backend.
    
    If a backend is not loaded before this is called, an AttributeError is raised.
    
    Backend:
        Backends should not change the values of display._BACKEND or display._INITIALIZED
    
    Return: None
    """
    try:
        _BACKEND.init()
        
    except AttributeError:
        raise AttributeError, "an event backend has not been loaded"
    
    global RECORD
    RECORD = EventRecord()


class EventRecord(dict):
    """Store a record of the current events for an action key, and the last event that happened if there are None.
    
    {
        action key: [[handled this frame, action type, time], [...]],
        K_m: [[1, KEYDOWN]], ## Keydown at _this_ frame
        K_f: [[1, KEYHOLD]], ## Keydown at sometime before, held since then
        K_g: [[1, KEYDOWN],  ## Keydown and Keyup _this_ frame
              [0, KEYUP]]
    }
    
    "Handled this frame" will be set to 1 if an EventListener runs a reaction for that event. This prevents multiple
    EventListeners from running reactions for the same event.
    """
    def __init__(self, *args):
        super(EventRecord, self).__init__(*args)
        
    def update_frame(self):
        """Update the EventRecord to reflect the curret events at the beginning of the frame.
        
        Updating Rules:
            1) Set all of the events to unhandled this frame.
            2) Update times of HOLDs.
            3) If there are any UP events for an action key, remove all events.
            4) If there are any DOWN events for an action key, replace each one with the equivalent HOLD action type at time of DOWN.
            5) If there are any posted events, remove them.
        
        Return: None
        """
        for key in self.keys():
            events = self[key]
            
            i = 0
            remove = []
            remove_all = False
            for event in events:
                event[0] = 0
                
                ## Remove leading NOEVENTS
                if event[1] == constants.NOEVENT:
                    if i == 0 and len(events) > 1:
                        remove.append(i)
                    i += 1
                    continue
                
                if event[1] == constants.MOUSEMOTION:
                    remove.append(i)
                    i += 1
                    continue
                
                ## Ignore Holds
                if (event[1] - 2) & (event[1] - 3) == 0:
                    i+= 1
                    continue
                
                ## Change "Ups" to "NOEVENTS"
                if (event[1] - 1) & (event[1] - 2) == 0:
                    remove_all = True
                    break
                
                ## Change "Downs" to "Holds"
                if event[1] & (event[1]-1) == 0:
                    self[key][i] = [0, event[1]+2]
                    i += 1
                    continue
                
                ## Remove "Posted"
                if (event[1] - 3) & (event[1] - 4) == 0:
                    remove.append(i)
                    i += 1
                    continue
                
                if (event[1] - 4) & (event[1] - 5) == 0:
                    remove.append(i)
                    continue
                
                if (event[1] - 5) & (event[1] - 6) == 0:
                    remove.append(i)
                    i += 1
                    continue
                    
                i += 1
            
            if remove_all:
                del self[key]
            
            if remove == []:
                continue
            
            offset = 0
            for j in remove:
                del self[key][j - offset]
                offset += 1
                
            if len(events) == 0:
                del self[key]
        
    def add(self, action_key, action_type):
        """Add an event to the EventRecord.
        
        Adding Rules:
            1) If adding a NOEVENT, just return
            2) Set "handled" to 0 in every case.
            3) If there isn't a key for action_key yet, create it.
        
            All other rules handle collisions:
                1) If there is a single NOEVENT, overwrite it.
        
                Adding UP:
                    1) If the last event is a HOLD, overwrite the last event
                    2) In any other situation, just append the UP event.
            
        
        Return: None
        """
        if action_type == constants.NOEVENT:
            return
        
        if not self.has_key(action_key):
            self[action_key] = []
            
        events = self[action_key]
        
        ## Handle single NOEVENT
        if len(events) == 1 and events[0][1] == constants.NOEVENT:
                self[action_key] = []
        
        ## Ups
        if (action_type-1) & (action_type-2) == 0:
            ## Overwrite HOLDS
            try:
                if (events[-1][1]-2) & (events[-1][1]-3) == 0:
                    self[action_key] = self[action_key][:-1]
            except IndexError:
                pass
                
        self[action_key].append([0, action_type])
        
    def add_posted(self, posted):
        for key in posted.keys():
            if not self.has_key(key):
                self[key] = []
                
            for event in posted[key]:
                self[key].append(event)
        

class Mods(object):
    """Holds information about modifiers for bound actions."""
    def __init__(self, include=[], exclude=[]):
        self.include = set(include)
        self.exclude = set(exclude)
        
    def match(self, record):
        """Return True actions in iterable work with self.include and self.exclude.
        
        Arguments:
            record - the EventRecord to check against.
        
        Return: boolean
        """
        if len(self.include) == 0 and len(self.exclude) == 0:
            return True
        
        for i in self.include:
            if i not in record:
                return False
            
            #try:
            #    if record[i][-1][1] == constants.NOEVENT:
            #        return False
            #except (TypeError, KeyError):
            #    return False
            
        for i in self.exclude:
            if i in record:
                #if record[i][-1][1] == constants.NOEVENT:
                #    continue
                
                return False
            
        return True
    

class EventListener(dict):
    """Delegates reactions to bound events.
    
    {
        (K_l, KEYDOWN): {
                            Mods: [delay, last_event, [(function, args), (function, args)]],
                            Mods: [delay, last_event, [(function, args), (function, args)]]
                        },
                              
        (K_a, KEYHOLD): {
                            ModifierPair: [delay, last_event, [(function, args), (function, args)]]
                        }
    }
    """
    def __init__(self, parent=None):
        super(EventListener, self).__init__()
        self.parent = None
        
    def bind(self, callback, act_key=constants.K_NONE, act_type=constants.NOEVENT, mods=None, delay=0, args=()):
        """Bind an action to the EventListener.
        
        Return: None
        """
        if act_type == constants.NOEVENT:
            return
        
        action = (act_key, act_type)
        if not self.has_key(action):
            self[action] = {}
        
        dest = None
        if mods is None:
            mods = Mods()
            
        for modpair in self[action]:
            if modpair.include == mods.include and modpair.exclude == mods.exclude:
                dest = self[action][modpair]
                break
        
        if dest is None:
            self[action][mods] = [delay, -delay, []]
            dest = self[action][mods]
        
        dest[2].append((callback, args))
    
    def unbind(self, act_key=constants.K_NONE, act_type=constants.NOEVENT, mods=None):
        """Unbind an action from the EventListner.
        
        If modpair is None, unbind all.
        
        Return: None
        """
        if act_key == constants.K_NONE and act_type == constants.NOEVENT:
            return
        
        action = (act_key, act_type)
        if not self.has_key(action):
            if act_key == constants.K_NONE:
                actions = []
                for key in self.keys():
                    if key[0] == act_key:
                        actions.append(key)
                        
                for action in actions:
                    self._unbind(action, mods)
                    return
            
            if act_type == constants.NOEVENT:
                actions = []
                for key in self.keys():
                    if key[1] == act_type:
                        actions.append(key)
                        
                for action in actions:
                    self._unbind(action, mods)
                    return
            
            raise KeyError, "Action {0} not bound.".format(action)
        
        self._unbind(action, mods)
        
    def _unbind(self, action, mods):
        if mods is None:
            self[action] = {}
            return
        
        if not self[action].has_key(mods):
            raise KeyError, "Action {0} bound, but ModifierPair {1} is not.".format(action, mods)
        
        del self[action][mods]
    
    def process(self):
        """Handle event processing for the current frame.
        
        1) Go through all events in the EventRecord and run bound reactions (check if event has been handled by a child, then check mods).
        2) If there is a parent, let it handle te rest of the EventRecord
        
        Return: None
        """
        keys = RECORD.keys()
        for key in keys:
            
            event_number = 0
            
            for event in RECORD[key]:
                if event[0] == 1:
                    event_number += 1
                    continue
                
                action = (key, event[1])                    
                if action[1] == constants.NOEVENT:
                    event[0] = 1
                    event_number += 1
                    continue
                
                if not self.has_key(action):
                    actionp = (key, event[1] - 3)
                    if self.has_key(actionp):
                        action = actionp
                    else:
                        event_number += 1
                        continue
                
                for modpair in self[action].keys():
                    if modpair.match(RECORD):
                        ## Check delays
                        if core._Data.current_time < self[action][modpair][0] + self[action][modpair][1]:
                            break
                        
                        self[action][modpair][1] = core._Data.current_time
                        
                        ## Run bound methods
                        for bound in self[action][modpair][2]:
                            bound[0](*bound[1])
                        
                        RECORD[key][event_number][0] = 1
                        break
                    
                event_number += 1
        
        if self.parent is not None:
            self.parent.process()
            

def process():
    """Handle event processing for the current frame.
    
    Return: None
    """
    RECORD.update_frame()
    for event in _BACKEND.poll():
        RECORD.add(event[0], event[1])
    RECORD.add_posted(POSTED)
    POSTED.clear()
    core._Data.eventlistener.process()

def post(action_key, action_type):
    """Manually add an event to be processed.
    
    Return: None
    """
    if action_type == constants.NOEVENT:
        return
    
    if not POSTED.has_key(action_key):
        POSTED[action_key] = []
    POSTED[action_key].append([0, action_type + 3, core._Data.current_time])
    
def bind(callback, act_key=constants.K_NONE, act_type=constants.NOEVENT, mods=None, delay=0, args=()):
    """Shortcut for binding an action to the current EventListener.
    
    Return: None
    """
    core._Data.eventlistener.bind(callback, act_key, act_type, mods, delay, args)
